Skip to content

Add David AI redelivered MFA pipeline from draco#2186

Open
timofeeva16tanya wants to merge 3 commits into
NVIDIA-NeMo:mainfrom
timofeeva16tanya:david_ai_preparation
Open

Add David AI redelivered MFA pipeline from draco#2186
timofeeva16tanya wants to merge 3 commits into
NVIDIA-NeMo:mainfrom
timofeeva16tanya:david_ai_preparation

Conversation

@timofeeva16tanya

Copy link
Copy Markdown

Sync current pipeline (RAM-by-session stages, glued-OOV heuristic detector, lexicon build, cluster submission scripts, ffmpeg timeout hardening) from the draco working copy.

Description

Usage

# Add snippet demonstrating usage

Checklist

  • I am familiar with the Contributing Guide.
  • New or Existing tests cover these changes.
  • The documentation is up to date with these changes.

Sync current pipeline (RAM-by-session stages, glued-OOV heuristic detector,
lexicon build, cluster submission scripts, ffmpeg timeout hardening) from
the draco working copy.
@timofeeva16tanya timofeeva16tanya requested a review from a team as a code owner July 8, 2026 13:41
@timofeeva16tanya timofeeva16tanya requested review from sarahyurick and removed request for a team July 8, 2026 13:41
@copy-pr-bot

copy-pr-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@greptile-apps

greptile-apps Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds the David AI redelivered MFA pipeline from the draco working copy as a new tutorial under tutorials/audio/david_ai_redelivered_mfa/. It introduces two parallel pipeline variants — one for opus audio and one for WAV — each with RAM-by-session processing, a glued-OOV heuristic detector, lexicon build helpers, ffmpeg timeout hardening, and cluster submission scripts.

  • Dual opus/WAV pipeline: Both variants share common helpers (david_ai_common.py, david_ai_mfa_align.py, david_ai_ram_session.py) with the WAV path adding an intermediate audio_masked_dir for 16 kHz masked WAVs before the session mix.
  • Timeout hardening: FFMPEG_TIMEOUT_S (env-configurable) guards every ffmpeg subprocess; MFA_ALIGN_TIMEOUT_S=3600 (hardcoded) now protects mfa align calls in both variants.
  • Resumable execution: Per-session .done flags and sessions_without_done_flags ensure restarts only reprocess incomplete sessions.

Confidence Score: 4/5

Safe to merge with one fix: the WAV variant's apply_white_noise_in_pause_intervals should add the same empty-pause short-circuit present in the opus variant to avoid unnecessary ffmpeg round-trips that can fail on dense-speech sessions.

The WAV pipeline's apply_white_noise_in_pause_intervals always decodes and re-encodes audio even when pause_intervals is empty, while the opus version correctly short-circuits to a file copy. On sessions where speech covers the full recording, this causes an avoidable ffmpeg call that can propagate TimeoutExpired and mark the session as failed when it should have succeeded.

tutorials/audio/david_ai_redelivered_mfa/wav/david_ai_common.py — the apply_white_noise_in_pause_intervals function needs the same empty-pause guard that the opus counterpart in opus/david_ai_common.py already has.

Important Files Changed

Filename Overview
tutorials/audio/david_ai_redelivered_mfa/wav/david_ai_common.py WAV-specific shared helpers; missing empty-pause short-circuit in apply_white_noise_in_pause_intervals means dense-speech sessions always pay an unnecessary ffmpeg decode/encode round-trip that can fail with TimeoutExpired.
tutorials/audio/david_ai_redelivered_mfa/opus/david_ai_common.py Opus-specific shared helpers; correctly implements empty-pause short-circuit and ffmpeg timeout handling; MFA_ALIGN_TIMEOUT_S in align file is not env-configurable unlike FFMPEG_TIMEOUT_S.
tutorials/audio/david_ai_redelivered_mfa/opus/david_ai_mfa_align.py MFA alignment with per-recording timeout (MFA_ALIGN_TIMEOUT_S=3600) and fallback-to-manifest logic; timeout is hardcoded unlike FFMPEG_TIMEOUT_S which reads from environment.
tutorials/audio/david_ai_redelivered_mfa/wav/david_ai_mfa_align.py Identical MFA alignment logic to the opus variant; same hardcoded MFA_ALIGN_TIMEOUT_S=3600 concern applies.
tutorials/audio/david_ai_redelivered_mfa/opus/stage_ram_session_pipeline.py Top-level pipeline orchestrator for opus path; correctly uses pending_sessions for task list, shard splitting, and resumable done-flag logic look correct.
tutorials/audio/david_ai_redelivered_mfa/wav/stage_ram_session_pipeline.py Top-level pipeline orchestrator for WAV path; same clean logic as the opus variant, correctly iterates only pending_sessions.
tutorials/audio/david_ai_redelivered_mfa/wav/david_ai_ram_session.py Per-session RAM worker for WAV pipeline; introduces audio_masked_dir for intermediate masked WAVs; session validation and done-flag logic look correct.
tutorials/audio/david_ai_redelivered_mfa/opus/david_ai_ram_session.py Per-session RAM worker for opus pipeline; lazy per-process MFA root initialization looks correct; audio handling goes through audio_mixed_dir only.

Reviews (3): Last reviewed commit: "new_vwersion" | Re-trigger Greptile

start = stage_names.index(stage_name)
except ValueError:
return
for name in stage_names[stage:]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 NameError on every call: the local variable is start but the slice uses the undefined name stage. Any caller of clear_stage_done_from will crash with NameError: name 'stage' is not defined at runtime.

Suggested change
for name in stage_names[stage:]:
for name in stage_names[start:]:

Comment on lines +294 to +295
for session_dir in sessions
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The task list iterates over sessions (all sessions) instead of todo_sessions (only the ones that need processing). The todo_sessions filter was correctly computed and logged, but is never used here. Every already-finished session is submitted to the process pool, where process_session_ram parses manifests and re-checks outputs before returning skipped=True — a significant waste at scale (e.g. a 200-node run re-processes every completed session on restart).

Suggested change
for session_dir in sessions
]
for session_dir in todo_sessions
]

Comment on lines +687 to +694
def recording_textgrid_paths(textgrid_dir: Path, recording_id: str) -> list[Path]:
ordinary = recording_textgrid_path(textgrid_dir, recording_id, variant="ordinary")
if ordinary.is_file():
return [ordinary]
fb_path = recording_textgrid_path(textgrid_dir, recording_id, variant="fb")
if fb_path.is_file():
return [ordinary, fb_path]
return [ordinary]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 When ordinary does not exist as a file but fb_path does, the function returns [ordinary, fb_path] — a list that includes the non-existent ordinary path. build_recording_rttm_lines iterates that list and calls parse_textgrid_words(ordinary), which raises an exception that is silently swallowed; only the fb path is parsed. The intent appears to be returning just the fb path (or both when both exist), but the current code returns a stale, missing path.

Suggested change
def recording_textgrid_paths(textgrid_dir: Path, recording_id: str) -> list[Path]:
ordinary = recording_textgrid_path(textgrid_dir, recording_id, variant="ordinary")
if ordinary.is_file():
return [ordinary]
fb_path = recording_textgrid_path(textgrid_dir, recording_id, variant="fb")
if fb_path.is_file():
return [ordinary, fb_path]
return [ordinary]
def recording_textgrid_paths(textgrid_dir: Path, recording_id: str) -> list[Path]:
ordinary = recording_textgrid_path(textgrid_dir, recording_id, variant="ordinary")
fb_path = recording_textgrid_path(textgrid_dir, recording_id, variant="fb")
if ordinary.is_file() and fb_path.is_file():
return [ordinary, fb_path]
if ordinary.is_file():
return [ordinary]
if fb_path.is_file():
return [fb_path]
return [ordinary]

from pathlib import Path
from typing import TypeVar

MFA_ROOT_DIR_DEFAULT = "/home/ttimofeeva/MFA_models"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The default MFA root path is a developer-specific home directory. Any user running this without setting MFA_ROOT_DIR will get a FileNotFoundError when resolving models, with a confusing path pointing to ttimofeeva's home. A generic fallback like ~/MFA_models is more appropriate.

Suggested change
MFA_ROOT_DIR_DEFAULT = "/home/ttimofeeva/MFA_models"
MFA_ROOT_DIR_DEFAULT = "~/MFA_models"

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +321 to +326
try:
result = subprocess.run(align_cmd, capture_output=True, text=True, env=mfa_env)
except OSError as exc:
logger.error("%s: mfa align failed to start: %s", recording_id, exc)
mfa_failed_globally = True
detail = str(exc)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing timeout on mfa align subprocess

subprocess.run(align_cmd, …) has no timeout= argument. The codebase already documents this exact failure mode: FFMPEG_TIMEOUT_S = 600 exists precisely because "a wedged ffmpeg (e.g. an internal futex deadlock seen when many run in a worker pool) blocks its caller forever and hangs the whole shard." MFA runs as another pooled subprocess with the same risk — if mfa align hangs (e.g. SQLite or pynini lock contention), the entire process pool worker blocks indefinitely with no recovery path. The outer try/except Exception in align_recording would catch a subprocess.TimeoutExpired and return RecordingAlignResult(ok=False), so adding a timeout here is safe and consistent.

Comment on lines +1321 to +1325
try:
start = stage_names.index(stage_name)
except ValueError:
return
for name in stage_names[stage:]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 NameError in clear_stage_done_from: stage vs start

The loop slices on stage, but the variable assigned by list.index() is named start. Any caller will crash with NameError: name 'stage' is not defined at runtime.

Suggested change
try:
start = stage_names.index(stage_name)
except ValueError:
return
for name in stage_names[stage:]:
try:
start = stage_names.index(stage_name)
except ValueError:
return
for name in stage_names[start:]:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant